﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using NETXPF.Library;
using System.IO;
using System.Web;

namespace NETXPF.Web
{
    /// <summary>
    /// Implements generic session-based authentication with user-name and password.
    /// Stores user name and password hash (SHA-256) of each user account in a JSON file (location specified by JSONPath property).
    /// Useful for simple secure authentication needed for any website.
    /// Can generally store quite a few users, probably up to 50,000.
    /// Cannot store custom user details, like role or profile info. For that kind of data, please link the user name to a primary key in a database.
    /// </summary>
    public static class Authentication
    {
        /// <summary>
        /// User account for signing in
        /// </summary>
        public class Account
        {
            /// <summary>
            /// User name
            /// </summary>
            public string UserName { get; set; }
            /// <summary>
            /// Password
            /// </summary>
            public string Password { get; set; }
        }

        /// <summary>
        /// Exception for when user is not authenticated but should be.
        /// </summary>
        public class AuthenticateException : Exception
        {
            /// <summary>
            /// Constructs a new AuthenticateException
            /// </summary>
            public AuthenticateException() : base() { }
            /// <summary>
            /// Constructs a new AuthenticateException with message
            /// </summary>
            public AuthenticateException(string msg) : base(msg) { }
        }

        /// <summary>
        /// Property specifying location of JSON data for user accounts.
        /// This defaults to "UserAccounts.json" in HostingEnvironment's App_Data directory.
        /// </summary>
        public static string JSONPath { get; set; }

        static Authentication()
        {
            try
            {
                JSONPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/UserAccounts.json");
            }
            catch (Exception) { }
        }

        /// <summary>
        /// Retrieves a list of users (each user is a Hashtable with key UserName and key PasswordHash)
        /// </summary>
        /// <returns></returns>
        public static ArrayList GetUsers()
        {
            return File.Exists(JSONPath) ? (ArrayList)JSON.Parse(File.ReadAllText(JSONPath)) : new ArrayList();
        }

        /// <summary>
        /// Changes user's password
        /// </summary>
        /// <param name="a"></param>
        /// <param name="newPassword"></param>
        public static void ChangePassword(Account a, string newPassword)
        {
            ArrayList users = GetUsers();
            bool change = false;
            foreach (Hashtable userObject in users)
            {
                if (userObject["UserName"].ToString().Equals(a.UserName) && userObject["PasswordHash"].ToString().Equals(Encryption.GetSHA256String(a.Password)))
                {
                    //modify password
                    userObject["PasswordHash"] = Encryption.GetSHA256String(newPassword);
                    change = true;
                    break;
                }
            }
            //save users
            if (change)
            {
                File.WriteAllText(JSONPath, JSON.JSONify(users));
            }
        }

        /// <summary>
        /// Creates a new user, using name-value pairs from POST data. (form fields must be named 'UserName' and 'Password')
        /// </summary>
        /// <param name="c"></param>
        public static void CreateUser(HttpContext c)
        {
            //take name-value pairs from context (form)
            CreateUser(new Account { UserName = c.Request.Form["UserName"], Password = c.Request.Form["Password"] });
        }

        /// <summary>
        /// Creates a new user, using specified credentials.
        /// </summary>
        /// <param name="a"></param>
        public static void CreateUser(Account a)
        {
            ArrayList users = GetUsers();
            users.Add(new { UserName = a.UserName, PasswordHash = Encryption.GetSHA256String(a.Password) });
            File.WriteAllText(JSONPath, JSON.JSONify(users));
        }

        /// <summary>
        /// Logs in the user by setting the session state. If user invalid, return false.
        /// </summary>
        /// <param name="c"></param>
        /// <param name="a"></param>
        /// <returns></returns>
        public static bool SignIn(HttpContext c, Account a)
        {
            ArrayList users = (ArrayList)JSON.Parse(File.ReadAllText(JSONPath));
            foreach (Hashtable userObject in users)
            {
                if (userObject["UserName"].ToString().Equals(a.UserName) && userObject["PasswordHash"].ToString().Equals(Encryption.GetSHA256String(a.Password)))
                {
                    c.Session["User"] = a;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Logs in the user with data from the form POST request ('UserName' and 'Password' fields).
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        public static bool SignIn(HttpContext c)
        {
            return SignIn(c, new Account { UserName = c.Request.Form["UserName"], Password = c.Request.Form["Password"] });
        }

        /// <summary>
        /// Logs out the user by removing session state.
        /// </summary>
        /// <param name="c"></param>
        public static void SignOut(HttpContext c)
        {
            c.Session.Remove("User");
        }

        /// <summary>
        /// Checks whether the user is currently signed in. Returns the user name, if any, or empty string otherwise.
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        public static string Authenticate(HttpContext c)
        {
            return c.Session["User"] == null ? "" : ((Account)c.Session["User"]).UserName;
        }

        /// <summary>
        /// Checks whether user is currently signed in. Throws an exception if not.
        /// </summary>
        /// <param name="c"></param>
        public static void AuthenticateWithException(HttpContext c)
        {
            if (c.Session["User"] == null)
            {
                throw new AuthenticateException("User is not signed in.");
            }
        }

        /// <summary>
        /// Checks whether user is currently signed in. If not, it redirects to the sign-in page URL.
        /// </summary>
        /// <param name="c"></param>
        /// <param name="signInURL"></param>
        public static void AuthenticateWithRedirect(HttpContext c, string signInURL)
        {
            if (c.Session["User"] == null)
            {
                c.Response.Redirect(signInURL);
            }
        }

        /// <summary>
        /// Gets current signed in user's details. This may contain the user's password, so please do not return this in the response.
        /// Use "Authenticate" if you want to send the client a response about whether the user is authenticated.
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        public static Account GetCurrentUser(HttpContext c)
        {
            return (Account)c.Session["User"];
        }
    }
}
